// Copyright 2009 Jan Hosang <jan.hosang@gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package gomake

import (
    "fmt"
    "strings"
    "path"
    "gomake/depgraph"
)

type goPkgs map[string] *goPkg
type goPkg struct {
    loc, name, output, label, objfile string
    visited, updated bool
    modTime uint64
    srcs []string
    deps []string
}

func (pkg *goPkg) Sources() []string { return pkg.srcs }
func (pkg *goPkg) Dependencies() []string { return pkg.deps }
func (pkg *goPkg) Label() string { return pkg.label }
func (pkg *goPkg) Output() string { return pkg.output }
func (pkg *goPkg) Temporaries() []string { return []string{pkg.objfile} }

func newGoPkgsFromFiles(files *goFiles) (pkgs *goPkgs) {
    pkgsObj := make(goPkgs)
    pkgs = &pkgsObj
    pkgs.addSrcs(files)
    return
}

func (pkgs *goPkgs) addToDepGraph(depGraph *depgraph.DependencyGraph) {
    for _, pkg := range *pkgs {
        depGraph.AddDependency(pkg)
    }
}

func (pkgs *goPkgs) addSrcs(files *goFiles) {
    deps := make(map[string] (map[string] bool))
    
    for fileName, fileObj := range *files {
        pkgName := fileName2pkgName(fileName, fileObj.pkg)
        pkg, found := (*pkgs)[pkgName]
        if !found {
            pkg = newGoPkg(pkgName)
            (*pkgs)[pkgName] = pkg
        }
        pkg.modTime = max(pkg.modTime, fileObj.modTime)
        pkg.appendSrc(fileName)
        
        // dependencies of source files are dependencies of
        // packages
        if len(fileObj.deps) > 0 {
            pkgDeps, found := deps[pkgName]
            if !found {
                pkgDeps = make(map[string] bool)
                deps[pkgName] = pkgDeps
            }
            
            for _, dep := range fileObj.deps {
                pkgDeps[dep] = true
            }
        }
    }
    
    // convert the map of dependencies into lists in the
    // package structs, throwing away non-local dependencies
    for pkgName, pkgDeps := range deps {
        depList := make([]string, len(pkgDeps))
        i := 0
        for dep, _ := range pkgDeps {
            if _, local := (*pkgs)[dep]; local {
                depList[i] = dep
                i += 1
            }
        }
        (*pkgs)[pkgName].deps = depList[0:i]
    }
    
    for _, pkg := range *pkgs {
        pkg.generateOutputAndTargetLabels()
    }
}

func (pkgs *goPkgs) AllTemporaries() []string {
    tmps := make(map[string] bool)
    for _, pkg := range *pkgs {
        for _, temp := range pkg.Temporaries() {
            tmps[temp] = true
        }
    }
    
    tmpList := make([]string, len(tmps))
    i := 0
    for temp, _ := range tmps {
        tmpList[i] = temp
        i += 1
    }
    return tmpList
}

func (pkgs *goPkgs) AllOutputs() []string {
    outputs := make([]string, len(*pkgs))
    i := 0
    for _, pkg := range *pkgs {
        outputs[i] = pkg.output
        i += 1
    }
    return outputs
}

func (pkg *goPkg) String() (repr string) {
    repr = fmt.Sprintf("%s builds %s from\n", pkg.label, pkg.output)
    for _, src := range pkg.srcs {
        repr += fmt.Sprintf("    %s\n", src)
    }
    if len(pkg.deps) > 0 {
        repr += fmt.Sprintf("    depends on %s\n", strings.Join(pkg.deps, ", "))
    }
    return
}

func (pkg *goPkg) generateOutputAndTargetLabels() {
    if pkg.name == "main" {
        pkg.label = pkg.fileName2executableName()
        pkg.output = "./"+pkg.label
    } else {
        pkg.output = path.Join(config.pkg, pkg.label+"."+compilerConfig.A)
    }
}

var unnamedMainCounter int = 0
func (pkg *goPkg) fileName2executableName() string {
    var target string
    target = config.target
    if len(target) == 0 {
        // try to guess the target name from the source names
        _, target = path.Split(pkg.srcs[0])
        for _, src := range pkg.srcs[1:len(pkg.srcs)] {
            _, src = path.Split(src)
            for i := 0; i < min(len(target),len(src)); i += 1 {
                if target[i] != src[i] {
                    target = target[0:i]
                    break
                }
            }
            if len(target) > len(src) {
                target = src
            }
        }
        if strings.HasSuffix(target, ".go") {
            target = target[0:len(target)-3]
        }
    }
    
    // there is no common prefix?
    if len(target) == 0 {
        // try the directory name it is located in
        target, _ = path.Split(pkg.srcs[0])
        _, target = path.Split(path.Clean(target))
    }
    
    // still unsucessful?
    if len(target) == 0 {
        target = fmt.Sprintf("main.%d", unnamedMainCounter)
        unnamedMainCounter += 1
    }
    
    return target
}

func fileName2pkgName(filename, pkg string) string {
    filepath, _ := path.Split(filename)
    
    // location of src/myLib/myPkg/myPkg.go should be
    //                 myLib/
    
    // so strip a directory at the end with the same name
    // of the package
    if shorterPath, topdir := path.Split(path.Clean(filepath)); topdir == pkg {
        filepath = shorterPath
    }
    
    // and strip the path of the src base at the beginning
    if strings.HasPrefix(filepath, config.src) {
        // strip the slash as well
        filepath = filepath[len(config.src)+1:len(filepath)]
    }
    
    return path.Join(filepath, pkg)
}

func newGoPkg(label string) (*goPkg) {
    pkg := new(goPkg)
    pkg.srcs = make([]string, 0, 2)
    pkg.label = label
    pkg.loc, pkg.name = path.Split(label)
    pkg.objfile = path.Join(config.obj, "_go_."+compilerConfig.O)
    return pkg
}

func (pkg *goPkg) appendSrc(src string) {
    pkg.srcs = append(pkg.srcs, src)
}

func append(slice []string, str string) []string {
    l :=  len(slice)
    if l + 1 > cap(slice) {
        newSlice := make([]string, (l+1)*2)
        for i, s := range slice {
            newSlice[i] = s
        }
        slice = newSlice
    }
    slice = slice[0:l+1]
    slice[l] = str
    
    return slice
}

func min(a, b int) int {
    if a <= b {
        return a
    }
    return b
}
